β

Как стать автором
Курсы по фронтенду
Айтишники, аттеншн! Выберите лучший IT-бренд 2025

Моя лентаВсе потоки

wikendp 12 дек 2022 в 01:04

Hashmap(map) по версии Golang вместе с реализацией на дженериках

Средний

12 мин

37K

Go*

Туториал

Из песочницы

Привет. Сегодня рассмотрим такую интересную структуру данных как hashmap, а именно ее реализацию в Go. Вкратце разберем что такое hashmap, как это выглядит под капотом Go 1.19. Посмотрим отличия реализации с Java и Python. Реализуем hashmap из-под капота с помощью дженериков.

Часть 2

Что такое hashmap

Для тех, кто еще не знаком с hashmap, прикладываю информацию из Википедии:
Hashmap - это структура данных, которая позволяет хранить пары ключ-значение. Ключи в hashmap уникальные. Можно представить как обычный массив, в котором для индекса можем использовать не только целые числа, но и, например, строки.

Сложность:

Операция

Средняя

Худшая

Вставка

Поиск

Удаление

Память

При углублении в реализацию такой структуры данных можно встретить следующие слова: хэш-функция, коллизии, бакеты, фактор заполненности. Разберемся что они значат:

Хэш-функция(Hash function). Под ней понимают функцию, которая принимает значение (ключ) неопределенного размера и возвращает значение фиксированной длины. В случае c Go она возвращает uint64. Одно из главных свойств - стабильность. Для одного и того же переданного значения она должна возвращать один и тот же результат;

Бакет(Bucket/Slot). Так называемая структура данных, в которой хранятся ключи и значения в мапе. В некоторых реализациях hashmap в одном бакете хранится одно значение, а в других - несколько. Например, в Go данные внутри бакета хранятся в массиве, и в одном бакете может быть до восьми элементов;

Коллизия (Collision). Так как хэш-функция не идеальна, передав в нее два разных значения мы можем получить один и тот же результат. В случае с бакетами нам нужно два разных значения положить в один и тот же бакет. Это называется коллизией. Для реализации hashmap необходимо иметь алгоритм их разрешения. Существует несколько таких алгоритмов (стратегий):

Closed addressing. Храним элементы с одинаковым хэшем с помощью дополнительных структур данных, таких как: связный список, двоичное дерево, массив и др. Используется в следующих языках: Go, Java, Scala;

Open addressing. В бакете хранится только одно значение. При возникновении коллизии выбирается следующий свободный бакет по какой-либо формуле. Такая стратегия используется в Python, Ruby, Rust, C++ и др;

Perfect hashing. Выбирается такая хэш-функция, при которой не будет коллизий. Подбирается для статичного, заранее известного набора ключей.

Фактор заполненности мапы (Overload factor). Это число (порог), превысив которое считается, что нужно увеличить количество бакетов (обычно вдвое) для сохранения константной скорости

Как hashmap реализована в Go

Упрощенный вариант работы hashmap в Go.

Рассмотрим по шагам упрощенный принцип работы hashmap. Для примера будем хранить оценки фильмов в формате название-оценка:

Передаем ключ "The Matrix" в хэш-функцию. Получаем uint64 - 18002143618149980261;

Вычисляем маску для наших бакетов. Она равна n-1, где n - количество бакетов. В примере 4 бакета, а маска равна 3;

Вычисляем номер бакета, в котором сохраним наше значение. Для этого используем битовое "и": hash & mask == 18002143618149980261 & 3 == 01 & 11(отбросили нули) = 01, что рано 1 в десятичной системе счисления;

Идем в бакет по индексу 1 и перебором проверяем массив на наличие нашего ключа. Если находим совпадение по ключу, то перезаписываем значение, иначе добавляем в первое свободное место;

Под капотом структуры мапы и бакета выглядят так:
runtime/map.go

// A header for a Go map.
type hmap struct {
count int // размер мапы. используется функцией len()
flags uint8
B uint8 // log_2 количества бакетов. Для 8 бакетов B=3, для 16 B=4 и так далее.
noverflow uint16 // примерное число переполненных бакетов
hash0 uint32 // seed для хэш-функции, генерируется при создании мапы. нужен для рандомизации хэш-функции

buckets unsafe.Pointer // ссылка на массив из 2^B бакетов; nil, если count==0
oldbuckets unsafe.Pointer // ссылка на массив предыдущих бакетов. в процессе роста мапы здесь будет массив старых бакетов, откуда потихоньку будут переноситься значения в новые бакеты.
nevacuate uintptr // количество "эвакуированных" бакетов.

extra *mapextra // опциональные поля
}

// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8 // массив tophash
// После массива tophash идет массив размера bucketCnt ключей и массив размера bucketCnt элементов.
}

В структуре бакета явно не описаны поля ключей, значений и ссылка на overflow бакет. Для получения этих значений используется адресная арифметика через unsafe.Pointer.

Интересности реализации:

В качестве ключа можно использовать любой тип данных для которого определена операция сравнения. Например, можно использовать структуру с тем же условием для всех ее полей;

При отсутствии элемента возвращается нулевое значение для типа. Вторым параметром можно получить флаг наличия элемента по ключу;

Нельзя получить адрес элемента. Потому что при росте мапы оно переедет в другой бакет и адрес у него, соответственно, поменяется;

Мапа не безопасна для конкурентного использования. Для этого можно использовать обертку из sync.Map или мьютекс;

Порядок итерации не сохраняется. При каждой новой итерации мапы последовательность возвращаемых элементов может отличаться. Под капотом каждый раз выбирается рандомный бакет, с которого начинается итерация. Для сохранения нужного порядка придется сохранять ключи в отдельном массиве и итерироваться по нему;

При каждом создании мапы генерируется seed для рандомизации хэш-функции. Это сделано для безопасности, так как зная хэш-функцию можно подобрать такие ключи, что все значения попадут в один бакет и мы получим линейную скорость поиска;

При коллизиях используется стратегия сlosed addressing. Мы перебираем все ячейки бакета (их 8) и ищем первое свободное место;

OverloadFactor равен 6.5 (~81% заполненности бакетов). Когда бакеты в среднем заполнены больше чем на 6.5 элементов, тригерится рост мапы, и все элементы перемещаются в новые бакеты, которых создается в два раза больше.

При росте элементы переносятся в новые бакеты постепенно, а не все сразу;

Некоторые отличия от реализаций в других языках

Python 3.6+. Подробнее можно посмотреть в очень интересном (правда) докладе Raymond Hettinger Modern Python Dictionaries (2017)

Сохраняется последовательность вставки;

При коллизиях свободный бакет ищется с помощью стратегии open addressing. Для оптимизации поиска свободного бакета используются следующие формулы: j = ((5*j) + 1) mod 2**i и j = (5*j) + 1 + perturb;

Данные хранятся отдельно от бакетов. Сами бакеты используются только как указатели (индексы) на данные. Выглядит это примерно так:

indices = [None, 1, None, None, None, 0, None, 2]
entries = [[-9092791511155847987, 'timmy', 'red'],
[-8522787127447073495, 'barry', 'green'],
[-6480567542315338377, 'guido', 'blue']]

Рядом с данными хранится полный хэш. Это позволяет не пересчитывать его при росте мапы.

LoadFactor - 2/3 = ~66%

Java. Разбор можно почитать в статье Внутренняя работа HashMap в Java:

При коллизиях используется стратегия сlosed addressing, но вместо массивов используется связный список. Также когда длина списка больше восьми он превращается в TreeMap. Это дает скорость поиска вместо как в случае со связным списком или массивом;

Все ключи должны быть объектами. Для этого примитивные типы (boolean, int, char, float и т.д) должны быть сконвертированы в объекты через boxing;

LoadFactor по умолчанию - 75%. При создании мапы можно указать свое значение параметра.

Также небольшое сравнение с реализациями Java и C++ можно посмотреть у Dave Cheney.

Кодирование

Реализуем базово hashmap из исходников Go 1.19. Не будем учитывать рост мапы, конкурентное обращение и возможность использовать разные типы для ключей.

Начнем с ведер

Определим структуру бакета.

Нам нужно два массива для ключей и значений, и ссылка на бакет для хранения дополнительных значений в случае переполнения текущего. Для оптимизации поиска храним массив первых восьми бит от хэша ключа, которые называются tophash.

const bucketSize = 8 // размер массивов внутри бакета

type bucket[T any] struct {
tophash [bucketSize]uint8 // массив первых восьми бит от хэша ключа; некоторые значения используем как флаги, подробности ниже.

keys [bucketSize]string // массив ключей
values [bucketSize]T // массив значений

overflow *bucket[T] // ссылка на бакет в случае переполнения текущего
}

Про tophash

В массиве с tophash мы резервируем несколько значений для дальнейшего использования. Ко всем значениям tophash, которые меньше minTopHash будем прибавлять его.

const (
emptyRest = 0 // эта и все следующие ячейки с бОльшим индексом пустые
emptyCell = 1 // ячейка пустая
minTopHash = 3 // минимальное значение tophash означающее что в ячейке есть значение
)

По умолчанию, в Go массив заполнен нулевыми значениями, соответственно для tophash массива, который типа uint8, будут нули. При добавлении или получении элемента из бакета в первую очередь ориентируемся на массив tophash, а не массив ключей. Это позволяет оптимизировать поиск внутри бакета.

Добавляем элемент в мапу

Алгоритм добавления элемента:

В цикле ищем совпадения по tophash и ключу или свободное место. Сохраняем данные и возвращаем соответствующий флаг для подсчета элементов в мапе.

Если не нашли нужного места, то повторяем упражнения на overflow бакете.

// Добавляем значение в бакет.
// Если переданные ключ уже присутствует в бакете, то значение перезапишется.
// Если бакет переполнен, то значение сохраняется в бакете overflow.
func (b *bucket[T]) Put(key string, topHash uint8, value T) (isAdded bool) {
var insertIdx *int

for i := range b.tophash {
// сравниваем tophash а не ключ, т.к. это позволяет нам использовать флаги и это работает быстрее
if b.tophash[i] != topHash {
if b.tophash[i] == emptyRest {
insertIdx = new(int)
*insertIdx = i
break
}

if insertIdx == nil && isCellEmpty(b.tophash[i]) {
insertIdx = new(int)
*insertIdx = i
}
continue
}

// tophash значения не уникальны, по этому при совпадении проверяем дополнительно и сам ключ
if b.keys[i] != key {
continue
}

b.values[i] = value
return false
}

// проверяем overflow бакет.
if insertIdx == nil || b.overflow != nil {
if b.overflow == nil {
b.overflow = &Bucket[T]{}
}

return b.overflow.Put(key, topHash, value)
}

// сохраняем ключ, значение и tophash
b.keys[*insertIdx] = key
b.values[*insertIdx] = value
b.tophash[*insertIdx] = topHash

// возвращаем признак успеха. по нему калькулируем количество элементов в мапе.
return true
}

// проверяем что значение tophash <= чем зарезервированное значение для пустой ячейки
func isCellEmpty(val uint8) bool {
return val <= emptyCell
}

Получаем элемент

Алгоритм поиска элемента такой же как и в методе Put. Дополнительно возвращаем флаг наличия элемента в бакете.

func (b bucket[T]) Get(key string, topHash uint8) (T, bool) {
for i := range b.tophash {
if b.tophash[i] != topHash {
// константа означает что все следующие ячейки пустые -- выходим из цикла.
if b.tophash[i] == emptyRest {
break
}
continue
}

if !isCellEmpty(b.tophash[i]) && b.keys[i] == key {
return b.values[i], true
}
}

// проверим бакет overflow
if b.overflow != nil {
return b.overflow.Get(key, topHash)
}

return *new(T), false
}

Удаляем элемент

Вместо фактического удаления просто помечаем ячейку пустой.

// Delete - удаляет элемент по переданному ключу
func (b *bucket[T]) Delete(key string, topHash uint8) (deleted bool) {
for i := range b.tophash {
if b.tophash[i] != topHash {
// если встречаем флаг все след. ячейки пустые, то просто выходим из функции
if b.tophash[i] == emptyRest {
return false
}
continue
}

// дополнительно проверяем совпадение по ключу, т.к. tophash не уникальный
if b.keys[i] == key {
b.tophash[i] = emptyCell
return true
}
}

// проверяем overflow бакет
if b.overflow != nil {
return b.overflow.Delete(key, topHash)
}

return false
}

Структура мапы

Храним размер мапы, количество бакетов, seed для хэш-функции и слайс самих бакетов.

// hmap - структура мапы
type hmap[T any] struct {
len int // количество реальных значений в мапе
b uint8 // log_2 от количества бакетов
seed uint64 // начальный хэш

buckets []Bucket[T] // слайс бакетов
}

// интерфейс hashmap, методы которого реализуем
type Hashmap[T any] interface {
Get(key string) T
Get2(key string) (T, bool)
Put(key string, value T)
Delete(key string)
Range(f func(k string, v T) bool)
Len() int
}

При инициализации генерируем seed и создаем нужное количество бакетов.

const (
// Максимальное среднее количество элементов в бакете которое тригерит рост мапы - 6.5
// Представлен как loadFactorNum/loadFactorDen, для того чтобы производить вычисления через int.
loadFactorNum = 13
loadFactorDen = 2

ptrSize = 4 << (^uintptr(0) >> 63) // размер указателя. используется для вычисления tophash
)

func New[T any](size int) Hashmap[T] {
h := new(hmap[T])

h.seed = generateSeed()

// тот самый log_2 от количества элементов
B := uint8(0)
// увеличиваем B пока средняя заполненность бакетов > loadFactor
for overLoadFactor(size, B) {
B++
}
h.b = B

h.buckets = make([]bucket[T], bucketsNum(h.b))

return h
}

// функция определяет будут ли бакеты, для size количества элементов, загружены больше чем loadFactor в среднем.
// этой же функцией потом будем определять нужно ли начать рост мапы
func overLoadFactor(size int, b uint8) bool {
return size > bucketSize && uint64(size) > loadFactorNum*(bucketsNum(b)/loadFactorDen)
}

// здесь b - log_2 от количества элементов
func bucketsNum(b uint8) uint64 {
return 1 << b
}

Get/Set/Delete

Основную логику мы заложили в модели бакета. Остается только вычислить tophash и номер бакета. Для put/delete учитываем изменение количества хранимых элементов.

// Get - возвращает значение по ключу key
// вернется нулевое значение типа <T> если по ключу key не существует значение.
func (h hmap[T]) Get(key string) T {
tophash, targetBucket := h.locateBucket(key)

v, _ := h.buckets[targetBucket].Get(key, tophash)
return v
}

// Get2 - возвращает значение по ключу key и флаг наличия этого значения в мапе
// вернет нулевое значение типа <T> и false если по ключу key не существует значения.
func (h hmap[T]) Get2(key string) (T, bool) {
tophash, targetBucket := h.locateBucket(key)

return h.buckets[targetBucket].Get(key, tophash)
}

// Put - добавляет элемент в мапу
func (h hmap[T]) Put(key string, value T) {
tophash, targetBucket := h.locateBucket(key)

if h.buckets[targetBucket].Put(key, tophash, value) {
h.len++
}
}

// Delete - удаляем элемент из мапы по переданному ключу
func (h hmap[T]) Delete(key string) {
tophash, targetBucket := h.locateBucket(key)
if h.buckets[targetBucket].Delete(key, tophash){
h.len--
}
}

Вычисление номера бакета и tophash:

// locateBucket - возвращает индекс бакета и tophash
func (h hmap[T]) locateBucket(key string) (tophash uint8, targetBucket uint64) {
hash := hash(key, h.seed)
tophash = topHash(hash)
mask := bucketMask(h.b)

targetBucket = hash & mask

return tophash, targetBucket
}

// возвращает первые 8 бит от значения
func topHash(val uint64) uint8 {
tophash := uint8(val >> (ptrSize*8 - 8))
if tophash < minTopHash {
tophash += minTopHash
}
return tophash
}

// bucketMask возвращает маску бакетов
func bucketMask(b uint8) uint64 {
return bucketsNum(b) - 1
}

// для хэширования используется алгоритм wyhash
func hash(key string, seed uint64) uint64 {
return wyhash.Hash([]byte(key), seed)
}

Итерация

Итерацию сделаем через callback функцию. Вместо оператора break будем использовать флаг возвращаемый callback функцией. Для рандомизации последовательности значений при итерации будем так же начинать со случайного бакета.

// Range - итерация по всем значениям с вызовом переданной функции
func (m hmap[T]) Range(f func(key string, value T) bool) {
for i := range m.randRangeSequence() {
bucket := &m.buckets[i]
for bucket != nil {
for j, th := range bucket.tophash {
// идет к след. бакету если видим флаг о пустых ячейках после индекса j
if th == emptyRest {
continue
}
// если в ячейке есть значение, то передаем в функцию
if th >= minTopHash {
// прерываем итерацию если получили false
if !f(bucket.keys[j], bucket.values[j]) {
return
}
}
}
// проверяем overflow бакеты
bucket = bucket.overflow
}
}
}

// формируем последовательность индексов для начала поиска со случайного бакета.
func (m hmap[T]) randRangeSequence() []int {
i := rand.Intn(len(m.buckets))

seq := make([]int, 0, len(m.buckets))
for len(seq) != len(m.buckets) {
seq = append(seq, i)
i++
if i >= len(m.buckets) {
i = 0
}
}

return seq
}

Тесты, бенчмарки

Исходники тестов можно посмотреть на Github. Ради интереса напишем бенчмарки для сравнения с мапой из коробки.
При сравнении методов Put и Get в пределах выделенной памяти разница достигает 20%:

var sizes = []int{128, 1024, 8192}
func BenchmarkGet(b *testing.B) {
for _, n := range sizes {
// выделяем память под n элементов
mm := New[int64](n)
for i := 0; i < n; i++ {
mm.Put(fmt.Sprintf("key__%d", i), int64(i)*2)
}

b.Run(fmt.Sprintf("generic-map_%d", n), func(b *testing.B) {
var got int64
j := 0
for i := 0; i < b.N; i++ {
if j > n {
j = 0
}
got = mm.Get(fmt.Sprintf("key__%d", j))
j++
}
_ = got
})
}
// такой же код для std мапы
}

go test . -run=^$ -bench . -benchmem
goos: darwin
goarch: arm64
pkg: github.com/w1kend/go-map
BenchmarkGet/generic-map_128-8 12884936 85.63 ns/op 8 B/op 1 allocs/op
BenchmarkGet/generic-map_1024-8 13559966 87.57 ns/op 14 B/op 1 allocs/op
BenchmarkGet/generic-map_8192-8 11720404 101.1 ns/op 22 B/op 1 allocs/op
BenchmarkGet/std-map_128-8 17141264 70.01 ns/op 8 B/op 1 allocs/op
BenchmarkGet/std-map_1024-8 16442701 72.42 ns/op 14 B/op 1 allocs/op
BenchmarkGet/std-map_8192-8 14521720 81.84 ns/op 22 B/op 1 allocs/op
BenchmarkPut/generic-map_128-8 16028941 74.27 ns/op 8 B/op 1 allocs/op
BenchmarkPut/generic-map_1024-8 15961150 75.22 ns/op 14 B/op 1 allocs/op
BenchmarkPut/generic-map_8192-8 12941882 85.13 ns/op 22 B/op 1 allocs/op
BenchmarkPut/std-map_128-8 16949132 70.37 ns/op 8 B/op 1 allocs/op
BenchmarkPut/std-map_1024-8 16461930 71.99 ns/op 14 B/op 1 allocs/op
BenchmarkPut/std-map_8192-8 14040560 82.28 ns/op 22 B/op 1 allocs/op

Переполнение мапы. Выделяем память на 1000 элементов и заполняем до 10 000, 100 000 и 1 000 000 элементов:

func BenchmarkPutWithOverflow(b *testing.B) {
startSize := 1_000
targetSize := []int{10_000, 100_000, 1_000_000}

for _, n := range targetSize {
mm := New[int64](startSize)
b.Run(fmt.Sprintf("generic-map-with-overflow__%d", n), func(b *testing.B) {
j := 0
for i := 0; i < b.N; i++ {
if j > n {
j = 0
}
mm.Put(fmt.Sprintf("key__%d", j), int64(j))
j++
}
})
}

for _, n := range targetSize {
stdm := make(map[string]int64, startSize)
b.Run(fmt.Sprintf("std-map-with-evacuation__%d", n), func(b *testing.B) {
j := 0
for i := 0; i < b.N; i++ {
if j > n {
j = 0
}
stdm[fmt.Sprintf("key__%d", j)] = int64(j)
j++
}
})
}
}

go test . -run=^$ -bench ^BenchmarkPutWithOverflow$ -benchmem
goos: darwin
goarch: arm64
pkg: github.com/w1kend/go-map
BenchmarkPutWithOverflow/generic-map-with-overflow__10000-8 11472094 104.9 ns/op 23 B/op 1 allocs/op
BenchmarkPutWithOverflow/generic-map-with-overflow__100000-8 3440781 344.7 ns/op 23 B/op 1 allocs/op
BenchmarkPutWithOverflow/generic-map-with-overflow__1000000-8 1000000 8376 ns/op 57 B/op 3 allocs/op
BenchmarkPutWithOverflow/std-map-with-evacuation__10000-8 14312827 83.43 ns/op 23 B/op 1 allocs/op
BenchmarkPutWithOverflow/std-map-with-evacuation__100000-8 12691999 90.62 ns/op 23 B/op 1 allocs/op
BenchmarkPutWithOverflow/std-map-with-evacuation__1000000-8 7283215 168.7 ns/op 23 B/op 1 allocs/op

Пока что такой бенчмарк не особо информативный, так как, очевидно, что результаты будут хуже. Интересно на сколько:

Увеличение элементов с 1000 до

Std map

Generic map

Разница

10 000

83.43 ns/op

104.9 ns/op

1.25

100 000

90.62 ns/op

344.7 ns/op

3.8

1 000 000

168.7 ns/op

8376 ns/op

49.65

На этом все, спасибо за внимание. Ставьте лайки, подписывайтесь на гитхаб.
В следующей части:

Научим мапу расти;

Замерим производительность при росте;

Добавим дженерик ключи;

Подумаем про конкурентное обращение.

И, наверное, стоит добавить что смысл данной статьи именно показать как hashmap реализована в Go под капотом, и показать более читаемый пример реализации на самом Go.

Ссылки

Исходники
Картинки гоферов
Гоферы от Ashley McNamara

Go:
GopherCon 2016: Keith Randall - Inside the Map Implementation
How the Go runtime implements maps efficiently (without generics)

Python:
Raymond Hettinger Modern Python Dictionaries A confluence of a dozen great ideas PyCon 2017 Raymond Hettinger. More compact dictionaries with faster iteration

Java:
Внутренняя работа HashMap в Java
The Java HashMap Under the Hood

Лекция cs166 stanford про Liner probing
An Analysis of Hash Map Implementations in Popular Languages

Теги:

go

hashmap

структуры данных

dictionary

map

Хабы:

Go

+31

127

9
+9

10

Карма

0

Рейтинг

Sergei Makarov @wikendp

Пользователь

Подписаться

Комментарии 9
+9

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.1K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.6K

Кейс

Перевод

+27

19

2
+2

lokosst 12 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.3K

Мнение

+19

17

59
+59

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.7K

Обзор

+16

29

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

Исследование: как и где ML-специалисты используют ИИ. Расскажите о своем опыте
Опрос

Показать еще

Минуточку внимания

Опрос

Да начнётся битва: выбираем лучший IT-бренд работодателя

Турбо

Полный свайп: оцени, подходит ли тебе работа в IT-ритейле

Событие

Хабр приглашает маркетологов и пиарщиков на тест-драйв

Курсы

Проектирование и реализация очередей в брокерах RabbitMQ и Apache Kafka
24 августа 2025Systems Education

Проектирование интеграции с REST API
31 августа 2025Systems Education

Проектирование и внедрение платформы данных - DAMA DMBOK на практике
2 сентября 2025BABOKSchool

Data science на языке Python
9 сентября 2025РЭУ им. Г. В. Плеханова

Проектирование и эксплуатация баз данных
16 сентября 2025BABOKSchool

Больше курсов на Хабр Карьере

Читают сейчас

Как превратить ноутбук в полноценное рабочее место не хуже стационарного ПК

12K

71
+71

Нашел ЛЯП в игре 5букв от Т-банка, который позволяет со 100% вероятностью угадать скрытое слово

54K

115
+115

Как выйти из IT?.. и пойти слесарем на завод. Моя попыточка дауншифтинга [4\5]

1.4K

11
+11

Моё мини-исследование про продолжительность жизни в России

6.3K

59
+59

Роскомнадзор направил операторам связи РФ письма о необходимости удаления ПО Speedtest Ookla из своей IT-инфраструктуры

21K

100
+100

Исследование: как и где ML-специалисты используют ИИ. Расскажите о своем опыте
Опрос

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Ближайшие события

7 августа

Мем-пати: как построить IT-бренд через вирусный контент и юмор

Москва

Больше событий в календаре

Маркетинг

13 августа

Как повысить эффективность врача в стоматологии на 25% с помощью ИИ?

Онлайн

Больше событий в календаре

Менеджмент

13 августа

Тест-драйв Хабра для маркетологов и пиарщиков

Москва

Больше событий в календаре

Маркетинг

16 августа

«ИТ-летник» — конференция от Совкомбанк Технологий

Казань

Больше событий в календаре

Разработка

Аналитика

3 сентября

«GoCloud Tech 2025» — IT-конференция про облака и AI

Москва • Онлайн

Больше событий в календаре

Разработка

18 – 19 сентября

Конференция по корпоративному онлайн-обучению iSpring Days 2025

Москва • Онлайн

Больше событий в календаре

Администрирование

Менеджмент

Другое

19 сентября

Премия для технологических стартапов Startech.Awards 2025

Москва • Онлайн

Больше событий в календаре

Другое

30 сентября

VII премия в сфере фармацевтического маркетинга и креатива MedMen Awards 2025

Онлайн

Больше событий в календаре

Маркетинг

Другое

30 октября

Премия «Проксима»

Москва

Больше событий в календаре

Маркетинг

Реклама

АО СЕЛЕКТЕЛ

ИНН 7810962785

erid: 2VSb5zDBFAH

1 000 000 стартапам

Кешбэк 30% на год в Selectel!

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr